import os, re
import pytz
from datetime import datetime
from urllib2 import unquote, urlopen
from bs4 import BeautifulSoup
import cStringIO
from django.utils import simplejson as json
from contextlib import nested
from django.shortcuts import render, redirect, get_object_or_404, render_to_response
from django.db.models import Q, F
from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseRedirect
from django.http import HttpResponse
from django.template import RequestContext
from django.template.loader import render_to_string
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import permission_required
from django.views.decorators.csrf import csrf_exempt
from django_socketio import broadcast_channel, NoSocket

from models import HWMode, IPSRepo, Location, Machine, MachineMore, \
                   MGroup, OSRelease, Site, HWConf, TestRun, Job, \
                   TestRunExec, DesignatedTestMachine
import const, conf, cmd, util, log


TITLE = {'neti': 'Net Install',
         'osu': 'OS Update',
         'dtu': 'DT Update',
         'exec': 'Test Run',
         'execsum': 'Execution Summary',
         'dashboard': 'Dashboard'}

def machine_list(request):
    host= request.GET.get('host', '')
    owner= request.GET.get('owner', '')
    groupname= request.GET.get('groupname', '')
    owner_list = User.objects.all()
    group_list = MGroup.objects.all()
    if host or owner or groupname:
        results = []
        if host:
            results = Machine.objects.filter(hostname__icontains=host).distinct()
        if owner:
            if results:
                results = results.filter(owner__username__icontains=owner) 
            else:
                results = Machine.objects.filter(owner__username__icontains=owner)
        if groupname:
            if results:
                results = results.filter(group__name__icontains=groupname)
            else:
                results = Machine.objects.filter(group__name__icontains=groupname)
        
        return render_to_response("machine_list.html", {"results": results, "owner_list":owner_list, "group_list":group_list}, context_instance=RequestContext(request))
    else:
        list = Machine.objects.all()
        return render_to_response("machine_list.html",{'results':list, 'owner_list':owner_list, 'group_list':group_list}, context_instance=RequestContext(request))

@login_required
def machine_add(request):
    if request.method == 'POST':
        form = MachineAdminForm(request.POST)
        if form.is_valid:
            machine = form.save()
            machine.save()
            return HttpResponseRedirect(reverse('machine_list'))
        else:
            return render_to_response("error.html", {
              'dj_err': "The Machine could not be created because the data didn't validate"}, 
              context_instance=RequestContext(request))
    else:
        form = MachineAdminForm()
    return render_to_response("machine_add.html",{'form':form}, context_instance=RequestContext(request))

@login_required
def machine_delete(request, machine_id):
    machine = get_object_or_404(Machine, pk=int(machine_id))
    machine.delete()
    return HttpResponseRedirect(reverse('machine_list'))

@login_required
def machine_update(request, machine_id):
    machine = get_object_or_404(Machine, pk=int(machine_id))
    if request.method == 'POST':
        form = MachineAdminForm(request.POST, instance=machine)
        if form.is_valid():
            machine = form.save()
            machine.save()
            return HttpResponseRedirect(reverse("machine_list"))
    return render_to_response("machine_update.html",{'form':MachineAdminForm(instance=machine)}, context_instance=RequestContext(request))

def machine_show(request, machine_id):
    machine = get_object_or_404(Machine, pk=int(machine_id))
    return render_to_response("machine_show.html", {'machine':machine}, context_instance=RequestContext(request))

def machine_search(request):
    host= request.POST.get('txt_host', '')
    owner= request.POST.get('sel_owner', '')
    groupname= request.POST.get('sel_group', '')
    results = []
    if host:
        results = Machine.objects.filter(hostname__icontains=host).distinct()
    if owner:
        if results:
            results = results.filter(owner__icontains=owner) 
        else:
            results = Machine.objects.filter(owner__icontains=owner).distinct()
    if groupname:
        if results:
            results = results.filter(group__name__icontains=groupname)
        else:
            results = Machine.objects.filter(group__name__icontains=groupname).distinct()
    
    return render_to_response("install_page.html", {"dj_results": results, "host": host, "owner":owner, "groupname":groupname}, context_instance=RequestContext(request))

@login_required
def reserve_add(request,machine_id):
    current_user = request.user
    current_machine = get_object_or_404(Machine,pk=int(machine_id))
    if current_machine.reserve_email =='':
        current_machine.reserve_email = current_user.email
        current_machine.save()
        return HttpResponseRedirect(reverse("machine_list"))
    else:
        return HttpResponseRedirect(reverse("machine_list"))

@login_required
def reserve_del(request,machine_id):
    current_user = request.user
    current_machine = get_object_or_404(Machine,pk=int(machine_id))
    if current_machine.reserve_email is None:
        return HttpResponseRedirect(reverse("machine_list"))
    elif current_machine.reserve_email == current_user.email:
        current_machine.reserve_email = ''
        current_machine.save()
        return HttpResponseRedirect(reverse("machine_list"))
    else:
        return HttpResponseRedirect(reverse("machine_list"))

def do_login(request):
    username = request.POST.get(u'username')
    password = request.POST.get(u'password')
    nexturl = request.POST.get(u'next', '/')

    if request.method == 'GET':
        return HttpResponseRedirect(reverse('login'))
        
    result = cmd.auth(username, password)[1]
    user = None
    if result == 'success':
        user_array = User.objects.filter(username__iexact=username)
        if user_array:
            user = user_array[0]
            user.set_password(password)
        else:
            user = User.objects.create_user(username, username, password)
        #user.save()
        User.objects.filter(pk=user.id).update(
          username=user.username, 
          email=user.email, 
          password=user.password)

        user = authenticate(username=username, password=password)
        if user is not None:
            if user.is_active:
                auth_login(request, user)
                return HttpResponseRedirect(nexturl)
            else:
                # return a 'disabled account' error message
                return render(request, 'error_login.html', {'result': 'disable account'})
        else:
            # return an 'invalid login' error message.
            return render(request, 'error_login.html', {'result': 'invalid login'})
    else:
        return render(request, 'error_login.html', {'result': result})


def account_logout(request):
    auth_logout(request)
    return HttpResponseRedirect(reverse('login'))

def account_register(request):
    return HttpResponseRedirect("http://testopia.cn.oracle.com/createaccount.cgi")

def ajax_getbnomgrp(request):
    if request.method != 'POST':
        return render(request, "error.html", {'dj_err': "Invalid Request"})

    site_id = request.POST.get(u'sel_site')
    os_rel = request.POST.get(u'sel_os_release')
    # get build list
    bl = util.get_bldlst(site_id, os_rel)
    # query machine list under os release
    gl = list(MGroup.objects.filter(os_release__name=os_rel).values('id', 'name'))

    return HttpResponse(json.dumps({'builds': bl, 'mgroups': gl}))
    
def ajax_getmlst(request, ltype):
    if request.method != 'POST':
        return render(request, "error.html", {'dj_err': "Invalid Request"})

    host_like = request.POST.get(u'txt_host')
    gid = request.POST.get(u'sel_mgroup')
    if gid:
        gid = int(gid)

    is_autochecked = False
    ml = None;
    context = {'dj_ltype': ltype,
               'dj_sbmbtn_name': TITLE[ltype],}

    tmpl = 'inst-mlst-ajax.html'
    if ltype == 'neti':
        site_id = int(request.POST.get(u'sel_site'))
        os_rel = request.POST.get(u'sel_os_release')
        #FIXME: is quering Machine enough here? If yes, 
        # just query Machine instead of MachineMore
        # query machine list 
        ml = MachineMore.objects.select_related(
               'machine',
               'machine__hwmode',
               'machine__mgroup',
               'machine__owner',
               'machine__site',
               'hwconf',
               'location').filter(machine__site_id=site_id)
        if host_like:
            ml = ml.filter(machine__hostname__contains=host_like)
        if gid:
            ml = ml.filter(machine__mgroup_id=gid)
        else:
            ml = ml.filter(machine__mgroup__os_release__name=os_rel)

        if len(ml) == 1 and \
           ml[0].machine.get_status() not in (
             const.MSTATUS_INSTALL,
             const.MSTATUS_UPDATE,
             const.MSTATUS_RUNNING):
            is_autochecked = True

        context['dj_machine_qs'] = ml
        context['dj_is_autochecked'] = is_autochecked
        return HttpResponse(render_to_string(tmpl, context))
    elif ltype == 'osu' or ltype == 'dtu':
        # for both osu and dtu
        ml = MachineMore.objects.select_related(
               'machine',
               'machine__hwmode',
               'machine__mgroup',
               'machine__owner',
               'machine__site',
               'hwconf',
               'location')
        if gid:
            ml = ml.filter(machine__mgroup_id=gid)
        else:
            gids = map(int, request.POST.get(u'hid_gids').split())
            ml = ml.filter(machine__mgroup_id__in=gids)
        if host_like:
            ml = ml.filter(machine__hostname__contains=host_like)

        # for osu/dtu, machine's status must be 'ready' or 'error'
        if len(ml) == 1 and \
           ml[0].machine.check_status() in (
             const.MSTATUS_READY,
             const.MSTATUS_ERROR):
            is_autochecked = True

        context['dj_machine_qs'] = ml
        context['dj_is_autochecked'] = is_autochecked
        return HttpResponse(render_to_string(tmpl, context))
    elif ltype == 'exec':
        # for execution
        tmpl = 'exec-mlst-ajax.html'
        start_type = int(request.POST.get(u'rdo_start_type'))
        context['dj_start_type'] = start_type

        # testrun list
        trl = None
        ml = MachineMore.objects.select_related(
               'machine',
               'machine__hwmode',
               'machine__mgroup',
               'machine__owner',
               'machine__site',
               'hwconf',
               'location').filter(machine__mgroup_id=gid)
        context['dj_machine_qs'] = ml
        if len(ml):
            # designated test machine list
            context['dj_dtm_lst'] = \
              list(DesignatedTestMachine.objects.filter(mgroup_id=gid).values_list('default', flat=True))
            context['dj_is_autochecked'] = is_autochecked
            trl = list(ml[0].machine.mgroup.testrun.all().order_by('priority').values('id', 'name'))

        return HttpResponse(json.dumps({'testruns': trl, 'ml': render_to_string(tmpl, context)}))
    

def install(request, inst_type):

    tmpl = ""
    context = {'dj_itype': inst_type,
               'dj_title': TITLE[inst_type],}
    if inst_type == 'neti':
        tmpl = 'neti.html'
        sites = Site.objects.only('id', 'name')
        os_rels = OSRelease.objects.all()
        this_site = util.get_site(request.META["REMOTE_ADDR"])
        context['dj_this_site'] = this_site
        context['dj_site_qs'] = sites 
        context['dj_os_release_qs'] = os_rels
    else:
        # for osu/dtu
        tmpl = 'osu.html'
        gl = MGroup.objects.filter(
              machine_type__in=(const.MACHINE_TYPE_AUTOTEST,
                                const.MACHINE_TYPE_MANTEST)).order_by('name')
        if inst_type == 'osu':
            pass
        elif inst_type == 'dtu':
            # dtu for pre-integration test only while osu
            # for both pre-integration and post-integration
            gl = gl.filter(intg_type=const.INTG_TYPE_PRE)

        context['dj_mgroup_qs'] = gl.values('id', 'name')

        mgrp_ids = ''
        for g in context['dj_mgroup_qs']:
            mgrp_ids += "%d " % g['id']
        context['dj_gids'] = mgrp_ids
    
    return render(request, tmpl, context)


@login_required
def execution(request):

    tmpl = 'execution.html'
    context = {'dj_exec': 'exec',}
    gl = MGroup.objects.filter(
           machine_type=const.MACHINE_TYPE_AUTOTEST).values('id', 'name').order_by('name')
    context['dj_mgroup_qs'] = gl
    return render(request, tmpl, context)


def report(request):
    return render(request, "report.html", {})


# return: (is_addclient, is_reboot, addclient_out)
def neti(os_rel, bno, mmobj, ipsrepobj):
    m = mmobj.machine
    # generate AI manifest
    aim = util.gen_aimanifest(m, ipsrepobj, os_rel, bno)
    # generate SC profile
    scp = util.gen_scprofile(m, ipsrepobj, os_rel)

    # save status related attributes in case there is rollback 
    saved_cur_stat = m.get_status()
    saved_pre_status = m.get_pre_status()
    saved_lsut = m.last_status_update_time

    # update machine's status as install
    m.set_status(const.MSTATUS_INSTALL)
    # reset latest status update time to now before the netinstall
    # even the machine is unreachable
    m.last_status_update_time = datetime.utcnow()
    #m.save(update_fields=['status', 'pre_status', 'last_status_update_time'])
    #m.save()
    Machine.objects.filter(pk=m.id).update(
      pre_status=m.pre_status,
      status=m.status,
      last_status_update_time=m.last_status_update_time)

    (retval, out, err) = cmd.addclient(
      os_rel,
      bno,
      m.hostname,
      m.site.domain,
      mmobj.mac,
      m.hwmode.get_arch_display().lower(),
      aim,
      scp)

    isaddclient = False
    isreboot = False
    if retval == 0:
        # addclient success
        isaddclient = True
        # try to reboot the remote machine if 
        # it's reachable and under wox control
        if saved_cur_stat in (const.MSTATUS_INITIAL, 
                              const.MSTATUS_NOTCTRL,
                              const.MSTATUS_UNREACHABLE): 
            isreboot = False
        else:
            # return value = 0 just means ssh executed successfully
            if cmd.reboot(m.hostname, m.site.domain)[0] == 0:
                isreboot = True
            else:
                isreboot = False
        # clear install log
        log.clear_inst_log(m.hostname)
        log.write_inst_log(m.hostname, """
Installation: %s
Hostname    : %s.%s
OS Release  : %s
Target Build: %s
""" % (TITLE['neti'], 
   m.hostname, 
   m.site.domain, 
   os_rel, 
   bno))

        # set cron job to delete client micro in DHCP table 
        # in half hour for x86 to avoid repeated installation
        if m.hwmode.arch == const.ARCH_X86:
            cmd.delclient(m.hostname, m.site.domain)

    else:
        # addclient failed, rollback machine status
        isaddclient = False
        m.last_status_update_time = saved_lsut
        m.rollback_status(saved_pre_status)
        #m.save(update_fields=['status', 'pre_status', 'last_status_update_time'])
        #m.save()
        Machine.objects.filter(pk=m.id).update(
          pre_status=m.pre_status,
          status=m.status,
          last_status_update_time=m.last_status_update_time)

    return (isaddclient, isreboot, out)


def do_neti(request):
    if request.method != 'POST':
        return render(request, "error.html", {'dj_err': "Invalid Request"})

    site_id = int(request.POST.get(u'sel_site'))
    os_rel = request.POST.get(u'sel_os_release')
    bld_no = request.POST.get(u'sel_build')
    mids = map(int, request.POST.getlist(u'ckb_machine'))
    machines = MachineMore.objects.select_related(
                 'machine',
                 'machine__site',
                 'machine__hwmode').filter(pk__in=mids)
    context = {'type': 'alert',}

    for mm in machines:
        m = mm.machine

        # check if the machine is reserved or not
        if m.reserve_email and m.reserve_email != user.email:
            context['content'] = \
              "Machine %s has been RESERVED by %s, you cannot install it now" % \
              (m.hostname, m.reserve_email)
            return HttpResponse(json.dumps(context))

        mstat = m.check_status()
        # check if machine is okey for (re)install
        if mstat in (const.MSTATUS_INSTALL, 
                     const.MSTATUS_UPDATE,
                     const.MSTATUS_RUNNING):
            context['content'] = \
              "Machine %s cannot be installed due to INVALID STATUS: %s" % \
              (m.hostname, m.get_status_display())
            return HttpResponse(json.dumps(context))

    # query ipsrepo from DB
    ipsrepo = IPSRepo.objects.get(site_id=site_id, os_release__name=os_rel)
    result = []

    # now it's okay for install, call addclient
    for mm in machines:
        m = mm.machine
        
        # call addclient and try to reboot machine
        (is_addclient, is_reboot, out) = neti(os_rel, bld_no, mm, ipsrepo)
        result.append({'hostname': m.hostname, 
                       'is_addclient': is_addclient, 
                       'addclient_msg': out, 
                       'is_reboot': is_reboot, 
                       'csl_url': util.get_cslurl(mm.sp_hostname, m.site.domain)})

    # end of for
    context['type'] = 'info'
    context['content'] = render_to_string(
                           'neti-result-ajax.html', 
                           {'dj_neti_result': result})
    return HttpResponse(json.dumps(context))


def osuordtu(os_rel, mobj, utype, bno='latest'):
    m = mobj

    # update machine's status as install
    m.set_status(const.MSTATUS_UPDATE)
    #m.save(update_fields=['status', 'pre_status'])
    #m.save()
    Machine.objects.filter(pk=m.id).update(
      pre_status=m.pre_status,
      status=m.status)

    if utype == 'osu':
        (retval, out, err) = cmd.osu(m.hostname, m.site.domain)
    elif utype == 'dtu':
        (retval, out, err) = cmd.dtu(m.hostname, m.site.domain, bno)

    log.clear_inst_log(m.hostname)
    log.write_inst_log(m.hostname, """
Installation    : %s
Hostname        : %s.%s
OS Release      : %s
Current OS Build: %s
Current DT Build: %s
""" % (TITLE[utype],
   m.hostname, 
   m.site.domain, 
   os_rel,
   m.os_bldno, 
   m.dt_bldno))

    if retval != 0:
        m.set_status(const.MSTATUS_ERROR)
        #m.save(update_fields=['status', 'pre_status'])
        #m.save()
        Machine.objects.filter(pk=m.id).update(
          pre_status=m.pre_status,
          status=m.status)
        log.write_inst_log(m.hostname, """
ERROR: run command cmd.%s() failed, please check if the machine is online

""" % utype)

    return (retval, out, err)


def do_osuordtu(request):
    if request.method != 'POST':
        return render(request, "error.html", {'dj_err': "Invalid Request"})

    itype = request.POST.get(u'hid_itype')
    mids_str = request.POST.getlist(u'ckb_machine')
    mids = map(int, mids_str)

    # note: select_related vs prefetch_related 
    # more mem occupied(space) vs. more db hits(time)
    machines = Machine.objects.select_related(
                 'site', 
                 'mgroup', 
                 'mgroup__os_release').filter(pk__in=mids)

    context = {'type': 'alert',}

    for m in machines:

        # check if the machine is reserved or not
        if m.reserve_email and m.reserve_email != user.email:
            context['content'] = \
              "Machine %s has been RESERVED by %s, you cannot install it now" % \
              (m.hostname, m.reserve_email)
            return HttpResponse(json.dumps(context))

        mstat = m.check_status()
        # check if machine is okey for update
        if mstat not in (const.MSTATUS_READY,
                         const.MSTATUS_ERROR):
            context['content'] = \
              "Machine %s cannot be installed due to INVALID STATUS: %s" % \
              (m.hostname, m.get_status_display())
            return HttpResponse(json.dumps(context))

    # now it's okay for update, call osu/dtu
    for m in machines:
        osuordtu(m.mgroup.os_release.name, m, itype)

    # end of for
    context['type'] = 'info'
    context['content'] = "/status/%s?mid=%s" % (itype, conf.URL_VS.join(mids_str))
    return HttpResponse(json.dumps(context))


def status_inst(request, inst_type=None):
    try:
        mids = map(int, request.GET.get(u'mid', '').split(conf.URL_VS))
    except ValueError, e:
        return render(request, "error.html", {'dj_err': "variable mid missing or invalid"})

    statustracks = []
    #machines = MachineMore.objects.select_related('machine', 'machine__site').filter(pk__in=mids)
    machines = MachineMore.objects.select_related('machine').filter(pk__in=mids).prefetch_related('machine__site')
    for mm in machines:
        m = mm.machine
        m.check_status()
        #FIXME: should we should status track box for all status
        trackinfo = {'id': log.get_logid(m.hostname),
                     'status': m.get_status_display(),
                     'title': "%s - " % m.hostname,
                     'cslurl': util.get_cslurl(mm.sp_hostname, m.site.domain),
                     'vncurl': util.get_vncurl(m.hostname, m.site.domain),
                     'osurl': util.get_osurl(m.hostname, m.site.domain),
                     'log': log.read_inst_log(m.hostname)}
        statustracks.append(trackinfo)

    return render(
      request,
      "statustrack.html", 
      {'dj_title': TITLE[inst_type] if inst_type else 'Install',
       'dj_statustracks': statustracks})


# Note: this function is not going to check the status of machine 
# and job, the check should be done before calling this function
def start_job(job, m, update_status_flag=True):
    if update_status_flag:
        if m.status != const.MSTATUS_RUNNING:
            # update machine status
            m.set_status(const.MSTATUS_RUNNING)
            #m.save(update_fields=['pre_status', 'status'])
            Machine.objects.filter(
              pk=m.id).update(pre_status=m.pre_status, 
                              status=m.status)

        if job.status != const.JSTATUS_STARTED:
            job.status = const.JSTATUS_STARTED
            job.start_time = datetime.utcnow()
            Job.objects.filter(pk=job.id).update(
                status=job.status,
                start_time=job.start_time)

    # update bno when job starts
    job.bldno = m.bldno
    q = job.queue.split(conf.WSA_MFS)
    tidq = map(int, q[::2])
    tnmq = q[1::2]

    # tre: testrunexec
    tre_strs = []
    tre = TestRunExec(job=job)
    # create all testruns in priority order
    for i, v in enumerate(tidq):
        tre.testrun_id = v
        tre.id = None
        tre.save()
        tre_strs.append("%d%s%s" % (tre.id, conf.WSA_MFS, tnmq[i]))

    # update queue, replace testrun ids with testrunexec ids
    job.queue = conf.WSA_MFS.join(tre_strs)
    #job.save(update_fields=['queue'])
    Job.objects.filter(pk=job.id).update(queue=job.queue, bldno=job.bldno)

    # start first testrun
    (first_tid, first_trname) = job.pop()
    cmd.runtest(m.hostname, 
                m.site.domain, 
                first_tid, 
                first_trname,
                job.dryrun_flag)
    #FIXME: notify if cmd is failed


@login_required
def do_exec(request):
    if request.method != 'POST':
        return render(request, "error.html", {'dj_err': "Invalid Request"})

    start_type = int(request.POST.get(u'rdo_start_type'))
    is_dryrun = True if u'ckb_isdryrun' in request.POST else False

    trids = map(int, request.POST.getlist(u'sel_testrun'))
    testruns = list(TestRun.objects.filter(pk__in=trids).order_by('priority').values('id', 'name'))
    # convert testruns into queue: testrun_id|name|testrun_id|name|...
    # note: we save testrun_id in Job.queue at beginning and will be
    # replaced with testrunexec ids when testrunexecs are created in DB
    tr_queue = conf.WSA_MFS.join([conf.WSA_MFS.join((str(i['id']), i['name'])) for i in testruns])

    mids = map(int, request.POST.getlist(u'ckb_machine'))
    mmachines = MachineMore.objects.select_related('machine').filter(pk__in=mids).prefetch_related('machine__site')
    # check if any machine is designated test machine, if yes, the job's dtm flag will be set to True
    dtms = list(DesignatedTestMachine.objects.filter(default_id__in=mids).values_list('default', flat=True))

    context = {'type': 'alert',}

    job = Job(start_type=start_type, 
              dryrun_flag=is_dryrun, 
              queue=tr_queue,
              owner=request.user)

    if start_type == const.JSTART_TYPE_NOW:
        # check again if machine is ready for test
        for mm in mmachines:
            m = mm.machine
            mstat = m.check_status()
            if mstat != const.MSTATUS_READY:
                context['content'] = \
                  "Machine %s is not ready for running test(STATUS: %s)" % \
                  (m.hostname, m.get_status_display())
                return HttpResponse(json.dumps(context))

        jobids = []
        for mm in mmachines:
            m = mm.machine

            # update machine status
            m.set_status(const.MSTATUS_RUNNING)
            #m.save(update_fields=['pre_status', 'status'])
            Machine.objects.filter(pk=m.id).update(pre_status=m.pre_status, status=m.status)

            job.test_machine = m
            job.hwconf_id = mm.hwconf_id
            job.mgroup_id = m.mgroup_id
            job.bldno = m.bldno
            job.dtm_flag = m.id in dtms
            # need to reset initial queue string here, because 
            # we use one object to create all jobs, and queue
            # is updated when previous job starts
            job.queue = tr_queue
            # create new jobs, one job per one machine
            job.id = None
            job.save()
            jobids.append(job.id)

            # start job
            start_job(job, m)

        context['type'] = 'redirect'
        context['content'] = "/status/testsum?jid=%s" % conf.URL_VS.join(map(str, jobids))
    elif start_type in (const.JSTART_TYPE_NEXT,
                        const.JSTART_TYPE_NEXTAFTER):
        # sometimes, key 'inp_nbafter' exists but it's null, POST.get return null
        try:
            afterhours = int(request.POST.get(u'inp_nbafter', '0'))
        except ValueError, e:
            afterhours = 0

        job.start_delay_hours = afterhours

        for mm in mmachines:
            m = mm.machine
            job.test_machine = m
            job.hwconf_id = mm.hwconf_id
            job.mgroup_id = m.mgroup_id
            job.dtm_flag = m.id in dtms
            # recored the current bldno on the machine, it'll 
            # be used to compare with the bldno after next 
            # installation to check if it's N + 1 build
            job.bldno = m.bldno
            # create new jobs, one job per one machine
            job.id = None
            job.save()

        context['type'] = 'info'
        context['content'] = 'Done. The test job(s) has/have been scheduled'
    elif start_type == const.JSTART_TYPE_FIXED:
        # check if the datetime is in the future
        # date format from client: Sun, 23 Dec 2012 13:20:00 GMT
        start_datetime_utc = datetime.strptime(
                           request.POST[u'hid_start_date_utc'], 
                           '%a, %d %b %Y %H:%M:%S %Z')
        if start_datetime_utc <= datetime.utcnow():
            context['content'] = \
              "It's too late to schedule the job at %s" % request.POST[u'hid_start_date_utc']
            return HttpResponse(json.dumps(context))

        jobids = []
        for mm in mmachines:
            m = mm.machine
            job.test_machine = m
            job.hwconf_id = mm.hwconf_id
            job.mgroup_id = m.mgroup_id
            job.dtm_flag = m.id in dtms
            # create new jobs, one job per one machine
            job.id = None
            job.save()
            jobids.append(job.id)

        # set cron job at the specified date/time
        # start_datetime_utc.strftime('%H:%M UTC %b %d, %Y')
        cmd.cron_jobs(start_datetime_utc.strftime('%H:%M UTC %b %d, %Y'), jobids)

        context['type'] = 'info'
        context['content'] = 'Done. The test job(s) has/have been scheduled'
    else:
        context['content'] = "Invalid start type %d" % start_type

    return HttpResponse(json.dumps(context))


def status_test(request):
    tids = request.GET.get(u'tid', '').split(conf.URL_VS)
    if not tids:
        return render(request, "error.html", {'dj_err': "variable tid missing"})

    tids = map(int, tids)
    statustracks = []
    trexecs = TestRunExec.objects.select_related('testrun').filter(pk__in=tids).prefetch_related('job')
    # test-machine dict
    tmd = {}
    # machinemore dict
    mmd = {}
    for t in trexecs:
        tmd[t.id] = t.job.test_machine_id
    mmachines = MachineMore.objects.select_related('machine', 'machine__stie').filter(pk__in=tmd.values())
    for mm in mmachines:
        mmd[mm.machine_id] = mm

    for t in trexecs:
        mm = mmd[tmd[t.id]]
        m = mm.machine
        trackinfo = {'id': log.get_logid(m.hostname, t.id),
                     'status': t.get_status_display(),
                     'title': "%s: %s - " % (m.hostname, t.testrun.name),
                     'cslurl': util.get_cslurl(mm.sp_hostname, m.site.domain),
                     'vncurl': util.get_vncurl(m.hostname, m.site.domain),
                     'osurl': util.get_osurl(m.hostname, m.site.domain),
                     'log': log.read_test_log(m.hostname, t.id)}
        statustracks.append(trackinfo)

    return render(
      request,
      "statustrack.html", 
      {'dj_title': TITLE['exec'],
       'dj_statustracks': statustracks})


def status_testsum(request):
    try:
        jids = map(int, request.GET.get(u'jid', '').split(conf.URL_VS))
    except ValueError, e:
        return render(request, "error.html", {'dj_err': "variable jid missing or invalid"})

    job_qs = Job.objects.select_related('test_machine', 
                                        'hwconf', 
                                        'hwconf__hwmode', 
                                        'mgroup').filter(pk__in=jids)
    jobs = []
    for j in job_qs:
        qf = j.queue.split(conf.WSA_MFS)
        hn = j.test_machine.hostname
        tids = qf[::2]
        tnms = qf[1::2]
        trs = []
        for i, nm in enumerate(tnms):
            trs.append({'id': log.get_logid(hn, tids[i]), 
                        'name': nm,
                        'status': TestRunExec(status=const.TSTATUS_SCHEDULED).get_status_display()})
        jobs.append({'db': j, 'testruns': trs, 'tidurl': conf.URL_VS.join(tids)})

    return render(
      request,
      'testsum.html', 
      {'dj_title': TITLE['execsum'], 'dj_jobs': jobs})


def percent(num, total):
    return int(round(num * 100.0 / total))

# get completion report via ajax
def ajax_getcreport(request):
    if request.method != 'POST':
        return render(request, "error.html", {'dj_err': "Invalid Request"})

    testopia_runids = request.POST.get(u'testopia_runids', '')
    try:
        tcn_guess = int(request.POST.get(u'hid_tcn_guess', '0'))
    except ValueError, e:
        tcn_guess = 0

    report_html = ""
    if testopia_runids:
        testopia_runids = json.loads(testopia_runids)
        rids_url = conf.URL_VS.join(testopia_runids.values())
        # completion report url
        crpt_url = conf.COMPLETION_REPORT_URL % rids_url
        # get testopia completion report page via urllib2
        urlfd = urlopen(crpt_url)
        # parse the page via bs4
        soup = BeautifulSoup(urlfd.read())
        urlfd.close()
        img = soup.find('img')
        if img:
            buglst_td = img.findParent('tr').findNextSibling('tr').findNext('td')
            # get total/completed/passed/failed/blocked case number 
            # img source e.g.: 
            #   tr_run_reports.cgi?type=bar&t=862&c=862&p=799&f=57&b=6
            nums = dict(num.split('=') for num in img['src'].split(r'&')[1::])
            # totoal case number
            tcn_byrun = int(nums['t'])
            tcn = tcn_guess if tcn_guess and tcn_guess > tcn_byrun else tcn_byrun
            # not completed case number
            nccn = tcn - int(nums['c'])
            trun_url = conf.RUN_LIST_URL % rids_url
            caserun_url = conf.CASERUN_LIST_URL % rids_url
            report_html = """
      <table>
      <tr>
      <td>
        <table id="completion_report_tbl">
          <tr><th>Total:</th><td colspan="2"><a href="%s" target="_blank">%d</a></td></tr>
          <tr><th>Completed:</th><td class="complete_cell"><a href="%s&case_run_status=PASSED&case_run_status=FAILED&case_run_status=BLOCKED" target="_blank">%s</a></td><td class="complete_cell">%d%%</td></tr>
          <tr><th>Not Complete:</th><td class="idle_cell"><a href="%s&case_run_status=IDLE&case_run_status=PAUSED&case_run_status=RUNNING&case_run_status=ERROR" target="_blank">%d</a></td><td class="idle_cell">%d%%</td></tr>
          <tr><th>PASSED:</th><td class="passed_cell"><a href="%s&case_run_status=PASSED" target="_blank">%s</a></td><td class="passed_cell">%d%%</td></tr>
          <tr><th>FAILED:</th><td class="failed_cell"><a href="%s&case_run_status=FAILED" target="_blank">%s</a></td><td class="failed_cell">%d%%</td></tr>
          <tr><th>BLOCKED:</th><td class="blocked_cell"><a href="%s&case_run_status=BLOCKED" target="_blank">%s</a></td><td class="blocked_cell">%d%%</td></tr>
        </table>
      </td>
      <td>
        <a href="%s" target="_blank">
        <img id='completion_report_img' src="%s&t=%d&c=%s&p=%s&f=%s&b=%s"/>
        </a>
      </td>
      </tr>
      <tr><!-- td of bug list -->%s</tr>
      </table>
""" % (trun_url, tcn,
       caserun_url, nums['c'], percent(int(nums['c']), tcn),
       caserun_url, nccn, percent(nccn, tcn),
       caserun_url, nums['p'], percent(int(nums['p']), tcn),
       caserun_url, nums['f'], percent(int(nums['f']), tcn),
       caserun_url, nums['b'], percent(int(nums['b']), tcn),
       crpt_url,
       conf.COMPLETION_BARCHART_URL, tcn, nums['c'], nums['p'], nums['f'], nums['b'],
       buglst_td)

    return HttpResponse(json.dumps({'report_html': report_html}))

def dashboard_html(gids, bldno):
    if not gids or not bldno:
        return 'No testrun found'

    # not available status name
    nastat_name = 'na'
    # not available status html string
    nastat_html = 'n/a'

    jobqs = Job.objects.select_related(
            'test_machine', 
            'hwconf__hwmode').filter(bldno=bldno).filter(mgroup_id__in=gids)
    jobqs = jobqs.filter(dryrun_flag=False).filter(dtm_flag=True)
    jobqs = jobqs.filter(status__in=(const.JSTATUS_STARTED, const.JSTATUS_FINISHED))
    jobqs = jobqs.order_by('start_time')
    if not len(jobqs):
        return 'No testrun found'

    dtmqs = DesignatedTestMachine.objects.select_related(
            'mgroup', 
            'mgroup__testrun', 
            'default').filter(mgroup_id__in=gids)
    # group objects
    gobjs = {}
    # machine objects
    mobjs = {}
    # testrun objects
    trobjs = {}
    # arch objects
    archobjs = {}
    for dtm in dtmqs:
        if dtm.mgroup_id not in gobjs:
            gobjs[dtm.mgroup_id] = dtm.mgroup
            trobjs[dtm.mgroup_id] = list(dtm.mgroup.testrun.all())
            trobjs[dtm.mgroup_id].sort(key=lambda tr: tr.priority)
        mobjs["g%d_a%d" % (dtm.mgroup_id, dtm.arch)] = dtm.default
        if dtm.arch not in archobjs:
            archobjs[dtm.arch] = dtm.get_arch_display()

    jobs = {}
    for j in jobqs:
        jobs[j.id] = j
        # the previous items might be overwritten, that's 
        # what we want to happen here
        mobjs["g%d_a%d" % (j.mgroup_id, j.hwconf.hwmode.arch)] = j.test_machine
        if j.hwconf.hwmode.arch not in archobjs:
            archobjs[j.hwconf.hwmode.arch] = j.hwconf.hwmode.get_arch_display()

    treqs = TestRunExec.objects.select_related('testrun').filter(job_id__in=jobs.keys()).order_by('id')
    # testrunexec objects
    treobjs = {}
    for t in treqs:
        k = "g%d_a%d" % (jobs[t.job_id].mgroup_id, jobs[t.job_id].hwconf.hwmode.arch)
        if k not in treobjs:
            treobjs[k] = {t.testrun.id: t}
        else:
            # the previous items might be overwritten, that's 
            # what we want to happen here
            treobjs[k][t.testrun.id] = t

    # let's print dashboard table
    html = cStringIO.StringIO()
    # print header row first
    html.write("""
    <table class="dashboard"><tbody>
      <input type="hidden" id="hid_runrpt_url" value="%s"/>
      <input type="hidden" id="hid_rawlog_url" value="%s"/>
      <input type="hidden" id="hid_tcn_guess" value="%d"/>
      <tr>
      <th>Machine Group</th>\n""" % ((conf.CASERUN_LIST_URL % '__RID_URL__'), 
                                     (conf.RAWLOG_URL % '__LOG_PATH__'),
                                     len(gobjs) * len(archobjs) * conf.MGROUP_CASENUM_GUESS))

    for ak in sorted(archobjs.iterkeys()):
        html.write(
"      <th>%s</th>\n" % archobjs[ak].capitalize())
    html.write(
'      <th>Summary</th>      </tr>\n')
 
    is_firstrow = True             
    # print dashboard table rows per mgroup sorted group name
    gobjs_sorted = gobjs.values()
    gobjs_sorted.sort(key=lambda g: g.name)
    for g in gobjs_sorted:
        html.write(
"      <tr>\n      <td>%s</td>\n" % g.name)
        # print dashboard table columns per arch
        for ak in sorted(archobjs.iterkeys()):
            html.write(
'      <td>\n')
            k = "g%d_a%d" % (g.id, ak)
            # test machine name
            tmn = mobjs[k].hostname

            # print each cell of dashboard table
            if k in treobjs:
                html.write(
'      <table class="testgroup" id="%s">\n' % log.get_testgid(tmn, bldno))
                # sort testrunexec list by testrunexec id
                treobjs_sorted = treobjs[k].values()
                treobjs_sorted.sort(key=lambda tre: tre.id)
                for tre in treobjs_sorted:
                    logid = log.get_logid(tmn, tre.id)
                    stat = tre.get_status_display()
                    html.write("""
        <tr>
        <td class="td-label">%s:</td>
        <td>
          <div id="%s" class="woxst-sum-bar woxst_%s">%s</div>\n""" \
                      % (tre.testrun.name,
                         logid, stat, stat))

                    if tre.log_path:
                        html.write(
'          <input type="hidden" id="%s-logdir" value="%s"/>\n' \
                          % (logid,
                             (conf.RAWLOG_URL % tre.log_path)))
                    if tre.report_id:
                        html.write(
'          <input type="hidden" id="%s-runid" value="%d"/>\n' \
                          % (logid, tre.report_id))

                    html.write(
'        </td>\n        </tr>\n')
                    # end of print testrunexecs

                html.write(
'      </table>\n')

            elif g.id in trobjs:
                # if there is no testrunexec against mgroup/arch, list mgroup's testruns
                html.write(
'      <table class="testgroup" id="%s">\n' % log.get_testgid(tmn, bldno))
                # tr list already be sorted by priority
                for tr in trobjs[g.id]:
                    html.write("""
        <tr>
        <td class="td-label">%s:</td>
        <td><div id="%s" class="woxst-sum-bar woxst_%s">%s</div></td>
        </tr>\n""" % (tr.name, 
                      log.get_natrid(tmn, tr.id),
                      nastat_name,
                      nastat_html))
                    # end of print testruns

                html.write(
'      </table>\n')
            else:
                pass

            html.write(
'      </td>\n')
            # end of for arch

        # print an empty summary cell(just once)
        # the contents would be updated via ajax
        if is_firstrow:
            is_firstrow = False
            html.write("""
      <td rowspan="%d">
      <div id="completion_report"></div>
      </td>\n""" % len(gobjs))
        # end of print summary cell

        html.write(
'      </tr>\n')
        # end of for mgroup

    html.write(
'    </tbody></table>\n')
    # end of dashboard table

    rv = html.getvalue()    
    html.close()
    return rv
             
# get dashboard mgroup
def ajax_getdbmgrp(request):
    if request.method != 'POST':
        return render(request, "error.html", {'dj_err': "Invalid Request"})

    osrid = request.POST.get(u'sel_os_release', '')
    # query machine list under os release
    gl = list(MGroup.objects.filter(
                os_release_id=osrid).filter(
                  machine_type=const.MACHINE_TYPE_AUTOTEST).values('id', 'name'))

    return HttpResponse(json.dumps({'mgroups': gl}))
 
def dashboard(request):
    gids = None
    bno = ''
    context = {}
    if request.method == 'GET':
        context['dj_title'] = TITLE['dashboard']
        gids = request.GET.get(u'gid', '').split(conf.URL_VS)
        bno = request.GET.get(u'bno', '')
        if not gids or not bno:
            context['dj_has_searchform'] = True
            context['dj_os_release_qs'] = OSRelease.objects.all().values('id', 'description')
        else:
            gids = map(int, gids)
            context['dj_has_searchform'] = False
            context['dj_dbhtml'] = dashboard_html(gids, bno)

        return render(request, 'dashboard.html', context)
    elif request.method == 'POST':
        # the request is through ajax
        gids = request.POST.getlist(u'sel_mgroup')
        if not gids:
            return HttpResponse('Invalid Request')

        gids = map(int, gids)

        bno = request.POST.get(u'sel_build', '')
        if not bno:
            # we need to get build list
            jobqs = Job.objects.filter(mgroup_id__in=gids)
            jobqs = jobqs.filter(dryrun_flag=False).filter(dtm_flag=True)
            jobqs = jobqs.filter(status__in=(const.JSTATUS_STARTED, const.JSTATUS_FINISHED))
            # put the latest build in head
            jobqs = jobqs.order_by('-bldno').distinct().values_list('bldno', flat=True)
            if jobqs:
                context['builds'] = list(jobqs)
                bno = context['builds'][0]

        # get dashboard table html
        context['dbhtml'] = dashboard_html(gids, bno)
        return HttpResponse(json.dumps(context))


# handle background autoinstall request
# only for autotest/mantest machine groups
@csrf_exempt
def do_bgai(request):
    if request.method != 'POST':
        return HttpResponse('Invalid Request', content_type="text/plain")

    # FIXME: authenticate client 

    os_rel = request.POST.get('r', '')
    # build type: (os|dt)
    try:
        btp = int(request.POST.get('t', '0'))
    except ValueError, e:
        btp = 0


    bno = request.POST.get('b', '')
    # placeholder
    #site = request.POST.get('s', '')
    if not os_rel or \
       not btp or \
       not bno:
        return HttpResponse('Invalid Post Data', content_type="text/plain")

    # FIXME: check if request has already been handled
    # FIXME: check if build is ready in the repo
         
    # group queryset
    gqs = MGroup.objects.select_related('testrun').filter(
             os_release__name__iexact=os_rel).filter(
             machine_type__in=(const.MACHINE_TYPE_AUTOTEST,
                               const.MACHINE_TYPE_MANTEST))

    update_type = None
    # install os build for both pre and post integration machines
    # only install dt build for preintegration machines
    if btp == const.BUILD_TYPE_DT:
        update_type = 'dtu'
        gqs = gqs.filter(intg_type=const.INTG_TYPE_PRE)
    else:
        update_type = 'osu'

    if not len(gqs):
        return HttpResponse('No machine matched', content_type="text/plain")

    gids = []
    postintg_gids = []
    tr_queues = {}
    for g in gqs:
        gids.append(g.id)
        if g.intg_type == const.INTG_TYPE_POST:
            postintg_gids.append(g.id)
        if g.machine_type == const.MACHINE_TYPE_AUTOTEST:
            trs = list(g.testrun.all())
            trs.sort(key=lambda tr: tr.priority)
            tr_queues[g.id] = conf.WSA_MFS.join(
              [conf.WSA_MFS.join((str(tr.id), tr.name)) for tr in trs])

    # get designated test machine list
    dtms = list(DesignatedTestMachine.objects.filter(
             mgroup_id__in=gids).values_list('default', flat=True))

    mmqs = MachineMore.objects.select_related(
                 'machine',
                 'machine__site',
                 'machine__hwmode').filter(machine__mgroup_id__in=gids)

    # output buffer
    outbf = cStringIO.StringIO()
    error_flag = False
    # we need to handle neti separately, because IPSRepo
    # is required and we just want to query it once
    neti_mms = []
    # record machine ids for those start installation
    inst_mids = []
    # record scheduled job ids
    jids = []
    for mm in mmqs:
        m = mm.machine
        # the machines that meet the following conditions will be autoinstalled
        #   o it's in auto/mantest groups
        #   o it's dtm or the autoinst_flag is true
        inst_flag = False
        if m.id in dtms:
            inst_flag = True
            # for designated test machine:
            # schedule job for regular test in DB anyway
            # even though the autoinst not start or fails
            # the job priority is high 2
            # the machines must meet the following conditions: 
            #   o it's dtm
            #   o in autotest group (i.e.: if in tr_queues dict)
            #   o in preintegration group if it's dt build or
            #     in postintegration group if it's os build
            if m.mgroup_id in tr_queues and \
               ( btp == const.BUILD_TYPE_DT or \
                 ( btp == const.BUILD_TYPE_OS and \
                   m.mgroup_id in postintg_gids )):
                job = Job(start_type=const.JSTART_TYPE_NEXT,
                          dryrun_flag=False,
                          owner_id=conf.DAEMON_USER_ID,
                          priority=2,
                          queue=tr_queues[m.mgroup_id],
                          test_machine_id=m.id,
                          hwconf_id=mm.hwconf_id,
                          mgroup_id=m.mgroup_id,
                          bldno=m.bldno,
                          dtm_flag=True,
                          id=None)
                job.save()
                jids.append(job.id)
                outbf.write("INFO: %s: create job %d\n" % (m.hostname, job.id))
        elif mm.autoinst_flag:
            inst_flag = True

        if inst_flag:
            stat = m.check_status()
            #FIXME: delay installation for running status
            if stat in (const.MSTATUS_ERROR,
                        const.MSTATUS_READY):
                # check if machine should take neti or update installation
                if btp == const.BUILD_TYPE_OS and \
                   mm.update_inst_times >= mm.update_inst_times_max:
                    # if OS build and update_inst_times >= max, do neti(do it later)
                    neti_mms.append(mm)
                else:
                    # otherwise, do update
                    # note: it's always update installation for DT build
                    osuordtu(os_rel, m, update_type, bno)
                    outbf.write("INFO: %s: start update installation\n" % m.hostname)
                    inst_mids.append(m.id)
            else:
                outbf.write("ERROR: %s: invalid status: %s, install not start\n" %  \
                            (m.hostname, m.get_status_display()))
                error_flag = True
    # end of for mmqs

    # handle neti 
    if neti_mms:
        # query IPSRepo first
        repoqs = IPSRepo.objects.filter(os_release__name__iexact=os_rel)
        # repo-site dict
        rsd = {}
        for r in repoqs:
            rsd[r.site_id] = r

        for mm in neti_mms:
            (is_addclient, is_reboot, out) = neti(os_rel, bno, mm, rsd[mm.machine.site_id])
            if is_addclient:
                if is_reboot:
                    outbf.write("INFO: %s: start neti installation\n" % mm.machine.hostname)
                else:
                    outbf.write("ERROR: %s: reboot failed\n" % mm.machine.hostname)
                    error_flag = True
                inst_mids.append(mm.machine.id)
            else:
                outbf.write("ERROR: %s: addclient failed\n" % mm.machine.hostname)
                error_flag = True

    out = '\n\n'
    if inst_mids:
        out += "check status of installation(s): \n%s\n\n" % \
          (request.build_absolute_uri("/status/inst?mid=%s" % (conf.URL_VS.join(map(str, inst_mids)))))
    if jids:
        out += "check status of scheduled test(s): \n%s\n\n" % \
          (request.build_absolute_uri("/status/testsum?jid=%s" % (conf.URL_VS.join(map(str, jids)))))
    out += outbf.getvalue()
    outbf.close()
    # send email to the notification alias
    util.sendemail("[WOX AI]: %s %s build %s - started %s" % \
                     (os_rel,
                      'dt' if btp == const.BUILD_TYPE_DT else 'os', 
                      bno,
                      'with error' if error_flag else ''),
                   out,
                   (conf.NOTIFY_ALIAS,))

    return HttpResponse(out, content_type="text/plain")

             
# handle background autotest request triggered by cronjob
# i.e. NEXTAFTER and FIXED. and it's only for autotest group machines 
@csrf_exempt
def do_bgat(request):
    if request.method != 'POST':
        return HttpResponse('Invalid Request', content_type="text/plain")

    # FIXME: authenticate client 
    
    jids = request.POST.get('jid', '')
    if not jids:
        return HttpResponse('Invalid Post Data', content_type="text/plain")
    jids = map(int, jids.split(conf.URL_VS))

    # update start_delay_hours as JOB_CRON_TIMEUP_FLAG
    # which indicates it's time to start this job. If 
    # test machine is not ready at this point, the job
    # will be started later 
    Job.objects.filter(
      pk__in=jids).filter(
      status=const.JSTATUS_SCHEDULED).update(
      start_delay_hours=const.JOB_CRON_TIMEUP_FLAG)

    jobqs = Job.objects.select_related(
              'test_machine', 
              'test_machine__site').filter(
              pk__in=jids).filter(
              status=const.JSTATUS_SCHEDULED).order_by('priority', 'id')
    # machine-job dict
    mjd = {}
    # group jobs by machine and order by priority/id
    for j in jobqs:
        if j.test_machine_id not in mjd:
            mjd[j.test_machine_id] = [j]
        else:
            mjd[j.test_machine_id].append(j)

    # start first job for each machine if machine
    # is ready for testing, otherwise, leave it 
    for i in mjd:
        j = mjd[i][0]
        m = j.test_machine
        if m.check_status() == const.MSTATUS_READY:
            start_job(j, m)
        

# assumption: 7-field build no. schema
#   e.g.: 5.12.0.0.0.4.0 or 0.175.1.0.0.5.0
#   f1,2: trunkid, e.g. 5.12
#   f3  : update
#   f4  : sru
#   f5  : platform
#   f6  : build no
#   f7  : nightly id
def is_nextbuild(prebno, newbno):
    rv = False
    if prebno == newbno:
        return rv

    bno_fs = '.'
    bno_fn = 7
    pbnof = prebno.split(bno_fs)
    nbnof = newbno.split(bno_fs)
    if len(pbnof) != bno_fn or \
       len(nbnof) != bno_fn:
        rv = True
    else:
        if pbnof[0] != nbnof[0] or \
           pbnof[1] != nbnof[1] or \
           pbnof[2] != nbnof[2] or \
           pbnof[3] != nbnof[3]:
            # different os releases
            rv = True
        else:
            # the first 5 fields are identical
            if pbnof[5] < nbnof[5] or \
               (pbnof[5] == nbnof[5] and pbnof[6] < nbnof[6]):
                rv = True

    return rv


#FIXME use lock and transaction here, e.g. select for update
# wsa do not send next wsa request until current request has
# been handled and curl get response, so it's okay even there
# is no locking mechanism
@csrf_exempt
def wsa(request, hostname):
    msg_key = 'wsamsg'
    if request.method != 'POST' or msg_key not in request.POST:
        return render(request, "error.html", {'dj_err': "Invalid Request"})

    # get messages from wsa by line
    msgs = unquote(request.POST.get(msg_key)).splitlines()

    m = None
    mqs = Machine.objects.select_related('mgroup', 'site').filter(hostname__iexact=hostname)

    if not len(mqs):
        return render(
                request, 
                "error.html", 
                {'dj_err': "Unrecognized machine %s" % hostname})
    else:
        m = mqs[0]
        # get the ip address by hostname via system naming service
        #FIXME: use socket.gethostbyname('jdstest30' + '.cn.oracle.com')
        valid_ip = cmd.getent(hostname, m.site.domain)[1]
        if not valid_ip or \
           valid_ip.split()[0] != request.META["REMOTE_ADDR"]:

            # the client IP address doesn't match hostname it sent,
            # reject the request
            return render(
              request, 
              "error.html", 
              {'dj_err': "Your IP address doesn't match hostname %s" % hostname})


    # update last_status_update_time anyway, if machine_update_flag
    # is True, it would be updated in DB then
    m.last_status_update_time = datetime.utcnow()

    # check machine status without update, update it later
    mstat = m.check_status(False)

    machine_update_flag = False
    push_inst_log_flag = False

    # cache testrunexec object to make sure just hit DB once
    test_cache = {}
    # store testrunexec object that need to be updated in DB
    test_update = {}
    # store testrunexec object, we need to push 
    # their status to web clients via websocket
    push_status = {}

    # handle the message line by line
    for l in msgs:
        util.debug(l)
        # split a message by WSA_MFS
        msg = l.split(conf.WSA_MFS)
        # WSA_UP
        if msg[0] == 'WSA_UP':
            # last_status_update_time would be updated
            machine_update_flag = True
        elif msg[0] == 'WSA_BNO':
            # update os/dt build no.
            osbno = msg[3]
            dtbno = msg[4]
            bno = None
            if not m.os_bldno or m.os_bldno != osbno:
                m.os_bldno = osbno
                machine_update_flag = True
            if not m.dt_bldno or m.dt_bldno != dtbno:
                m.dt_bldno = dtbno
                machine_update_flag = True
            if m.mgroup.intg_type == const.INTG_TYPE_PRE:
                bno = m.dt_bldno
            else:
                bno = m.os_bldno
            if not m.bldno or m.bldno != bno:
                m.bldno = bno
                machine_update_flag = True
        elif msg[0] == 'WSA_HWC':
            # update hardware configuration

            # physical processor no.
            pno = int(msg[1])
            # virtual processor no.
            vno = int(msg[2])
            # processor clock speed
            cks = msg[3]
            # memory size
            msize = msg[4]

            mm = MachineMore.objects.select_related('hwconf').get(pk=m.id)
            # check if it's necessary to create new hwconf in DB
            # and assign to mm.hwconf
            if (not mm.hwconf) or \
               mm.hwconf.psr_pnumber != pno or \
               mm.hwconf.psr_vnumber != vno or \
               mm.hwconf.psr_ckspeed != cks or \
               mm.hwconf.mem_size != msize:
                mm.hwconf = HWConf.objects.create(
                  hwmode_id = m.hwmode_id,
                  psr_pnumber = pno,
                  psr_vnumber = vno,
                  psr_ckspeed = cks,
                  mem_size = msize)

                mm.save()
                
        # WSA_PATROL
        elif msg[0] == 'WSA_PATROL':
            # last_status_update_time would be updated
            machine_update_flag = True
        # WSA_PONG
        elif msg[0] == 'WSA_PONG':
            # last_status_update_time would be updated
            machine_update_flag = True
        # CSC
        elif msg[0] == 'CSC':
            if msg[1] == 'LOG1':
                log.write_inst_log(m.hostname, "%s\n" % msg[2])
                push_inst_log_flag = True
            elif msg[1] == 'EXIT_CODE':
                exit_code = int(msg[2])
                # exit code: 
                #   0, 141: okay to move on
                #   7, 8: the build has already been installed, installation skipped
                #   other: installation failed
                if exit_code not in (0, 141,):
                    if exit_code in (7, 8,):
                        next_status = const.MSTATUS_READY
                    else:
                        next_status = const.MSTATUS_ERROR
                    m.set_status(next_status)
                    machine_update_flag = True
                    push_inst_log_flag = True
                    log.flush_inst_log(m.hostname)
        # CTE
        elif msg[0] == 'CTE':
            # we don't care CTE msg if the status is install/update/error
            # if in error status, we probably fix the issue and restart
            # cte service 
            if mstat not in (const.MSTATUS_INSTALL, 
                             const.MSTATUS_UPDATE,
                             const.MSTATUS_ERROR):
                continue

            if msg[1] == 'START':
                if mstat == const.MSTATUS_ERROR:
                    # cte service get restarted since last failure, we assume
                    # the problem get fixed and the install/update is continued
                    # set status as install/update, and previous status as error
                    m.rollback_status(const.MSTATUS_ERROR)
                    machine_update_flag = True
                    push_inst_log_flag = True
            elif msg[1] == 'LOG1':
                log.write_inst_log(m.hostname, "%s\n" % msg[2])
                push_inst_log_flag = True
            elif msg[1] == 'EXIT_CODE':
                # FIXME: send notification by email
                cte_status = int(msg[3])
                exit_code = int(msg[2])
                if cte_status == const.CTESTATUS_FINISH:
                    # exitcode == 9: the build has already been installed, installation skipped
                    if exit_code in (9, ):
                        m.set_status(const.MSTATUS_READY)
                        machine_update_flag = True
                        push_inst_log_flag = True
                        util.sendemail("[WOX INST]: %s - skipped" % (m.hostname),
                                       "%s\n\nmore details: %s\n\n%s\n" % \
                                         (log.read_inst_log(m.hostname),
                                          request.build_absolute_uri("/status/inst?mid=%d" % m.id),
                                          check_job_content),
                                       (conf.NOTIFY_ALIAS,))

                        # flush log from memory into file and remove the cache string
                        log.flush_inst_log(m.hostname, "CTE INFO: Installation is skipped!\n")
                    else:
                        # the install/update and cte deployment are finished
                        # update timestamp in MachineMore

                        if mstat == const.MSTATUS_INSTALL:
                            MachineMore.objects.filter(pk=m.id).update(
                              update_inst_times=0)
                        elif mstat == const.MSTATUS_UPDATE:
                            MachineMore.objects.filter(pk=m.id).update(
                              update_inst_times=F('update_inst_times') + 1)
                        m.set_status(const.MSTATUS_READY)
                        machine_update_flag = True
                        push_inst_log_flag = True

                        # start scheduled job
                        jobqs = Job.objects.filter(
                              test_machine_id=m.id).filter(
                              status=const.JSTATUS_SCHEDULED).filter(
                              start_type__in=(const.JSTART_TYPE_NEXT,                              
                                              const.JSTART_TYPE_NEXTAFTER,
                                              const.JSTART_TYPE_FIXED))
                        jobqs = jobqs.order_by('priority', 'id')
                        firstjob = None
                        for j in jobqs:
                            if j.start_type == const.JSTART_TYPE_NEXT:
                                if not firstjob and is_nextbuild(j.bldno, m.bldno):
                                    firstjob = j
                            elif j.start_type == const.JSTART_TYPE_NEXTAFTER:
                                # set cronjob for triggering NEXTAFTER job
                                cmd.cron_jobs("now + %d hours" % j.start_delay_hours, (j.id,))
                            elif j.start_type == const.JSTART_TYPE_FIXED and \
                                 not firstjob and \
                                 j.start_delay_hours == const.JOB_CRON_TIMEUP_FLAG:
                                firstjob = j

                        if firstjob:
                            start_job(firstjob, m)

                        # send notification to machine owner
                        check_job_content = ''
                        if firstjob:
                            check_job_content = \
                              "The scheduled test job has been started, check status:\n%s\n" % \
                              request.build_absolute_uri("/status/testsum?jid=%d" % firstjob.id)

                        util.sendemail("[WOX INST]: %s - complete" % (m.hostname),
                                       "%s\n\nmore details: %s\n\n%s\n" % \
                                         (log.read_inst_log(m.hostname),
                                          request.build_absolute_uri("/status/inst?mid=%d" % m.id),
                                          check_job_content),
                                       (conf.NOTIFY_ALIAS,))

                        # flush log from memory into file and remove the cache string
                        log.flush_inst_log(m.hostname, "CTE INFO: Installation is completed!\n")
                elif cte_status in (const.CTESTATUS_FAIL,
                                   const.CTESTATUS_NOA, 
                                   const.CTESTATUS_AMB, 
                                   const.CTESTATUS_ERR): 
                    if cte_status != const.MSTATUS_ERROR:
                        m.set_status(const.MSTATUS_ERROR)
                    machine_update_flag = True
                    push_inst_log_flag = True

                    util.sendemail("[WOX INST]: %s - error" % (m.hostname),
                                   "%s\n\nmore details: %s\n" % \
                                     (log.read_inst_log(m.hostname),
                                      request.build_absolute_uri("/status/inst?mid=%d" % m.id)),
                                   (conf.NOTIFY_ALIAS,))

                    # flush log from memory into file and remove the cache string
                    log.flush_inst_log(m.hostname, "CTE ERROR: Installation is failed!\n")
                else:
                    # cte deployment needs a reboot, it will continue
                    # after reboot, do nothing here
                    pass
        # TID
        elif msg[0] == 'TID' and msg[1]:
            tid = int(msg[1])
            if tid not in test_cache:
                # do select_related query to get job object
                #FIXME: catch DoesNotExist exception
                test_cache[tid] = TestRunExec.objects.select_related('job').get(pk=tid)
            test = test_cache[tid]
        
            if msg[2] == 'START':
                test.status = const.TSTATUS_STARTED
                test.start_time = datetime.utcnow()
                test.log_path = msg[3]

                # query update, just hit DB once
                TestRunExec.objects.filter(pk=tid).update(
                  status = test.status,
                  start_time = test.start_time,
                  log_path = test.log_path)

                push_status[tid] = test
            elif msg[2] == 'LOG1':
                log.write_test_log(m.hostname, tid, "%s\n" % msg[3])
                push_status[tid] = test
            elif msg[2] == 'LOG2':
                log.write_test_log(m.hostname, tid, msg[3])
                push_status[tid] = test
            elif msg[2] == 'REPORT_ID':
                # note: 1. exit_code msg is always following report_id msg
                #       2. report_id msg is not always there, e.g. dryrun
                try:
                    test.report_id = int(msg[3])
                except ValueError, e:
                    print "WARNING: %s" % e
                else:
                    test_update[tid] = test
            elif msg[2] == 'EXIT_CODE':
                # flush log from memory into file and remove the cache string
                log.flush_test_log(m.hostname, tid)

                exit_code = int(msg[3])
                if exit_code == 0:
                    test.status = const.TSTATUS_FINISHED
                elif exit_code == 103:
                    test.status = const.TSTATUS_TIMEOUT
                else: 
                    test.status = const.TSTATUS_ERROR
                test.end_time = datetime.utcnow()
                #test.save(update_fields=['report_id', 'status', 'end_time'])
                #test.save()
                TestRunExec.objects.filter(pk=tid).update(
                  report_id=test.report_id,
                  status=test.status,
                  end_time=test.end_time)
                # test object already saved, so remove it from test_update
                if tid in test_update:
                    del test_update[tid]

                push_status[tid] = test

                # get the next testrun from queue
                (next_tid, next_test_name) = test.job.pop(tid)
                if next_tid:
                    # next testrun exists, run it
                    cmd.runtest(m.hostname, 
                                m.site.domain, 
                                next_tid, 
                                next_test_name,
                                test.job.dryrun_flag)
                    #FIXME: check return value
                else:
                    # all testrun in queue are finished
                    # set job status
                    test.job.status = const.JSTATUS_FINISHED
                    test.job.end_time = datetime.utcnow()
                    #test.job.save(update_fields=['status', 'end_time'])
                    #test.job.save()
                    Job.objects.filter(pk=test.job_id).update(
                      status=test.job.status,
                      end_time=test.job.end_time)

                    # send notification
                    to = []
                    cc = []
                    if test.job.owner_id == conf.DAEMON_USER_ID:
                        to.append(conf.NOTIFY_ALIAS)
                    else:
                        jownerqs = User.objects.filter(pk=test.job.owner_id).only('email')
                        if len(jownerqs):
                            to.append(jownerqs[0].email)
                            cc.append(conf.NOTIFY_ALIAS)
                        else:
                            to.append(conf.NOTIFY_ALIAS)
                    util.sendemail("[WOX TEST]: job %d on %s - complete" % (test.job.id, m.hostname),
                                   "\n\ncheck status: %s\n\n" %
                                     request.build_absolute_uri("/status/testsum?jid=%d" % test.job.id),
                                   to,
                                   cc)

                    # start the next scheduled job if exists
                    nextjob = None
                    nextjobqs = Job.objects.filter(
                                  test_machine_id=m.id).filter(
                                  status=const.JSTATUS_SCHEDULED).filter(
                                  start_type__in=(const.JSTART_TYPE_NEXT,
                                                  const.JSTART_TYPE_NEXTAFTER,
                                                  const.JSTART_TYPE_FIXED)).order_by('priority', 'id')
                    for j in nextjobqs:
                        if j.start_type == const.JSTART_TYPE_NEXT:
                            # start job if it's next build
                            if is_nextbuild(j.bldno, m.bldno):
                                nextjob = j
                                break
                        elif j.start_type == const.JSTART_TYPE_NEXTAFTER:
                            # start job if it's next build and it's timeup
                            if is_nextbuild(j.bldno, m.bldno) and \
                               j.start_delay_hours == const.JOB_CRON_TIMEUP_FLAG:
                                nextjob = j
                                break
                        elif j.start_type == const.JSTART_TYPE_FIXED:
                            # start job if it's timeup
                            if j.start_delay_hours == const.JOB_CRON_TIMEUP_FLAG:
                                nextjob = j
                                break
   
                    if nextjob:
                        start_job(nextjob, m)
                    else:
                        # no job any more, so machine is free now
                        # set m.status
                        m.set_status(const.MSTATUS_READY)
                        machine_update_flag = True

    # end of for

    for k in test_update.keys():
        #test_cache[k].save(update_fields=['report_id'])
        #test_cache[k].save()
        TestRunExec.objects.filter(pk=tid).update(
          report_id=test_update[k].report_id)
 
    # update machine object in DB if necessary
    if machine_update_flag:
        #m.save(update_fields=['last_status_update_time', 'status', 'pre_status', 'os_bldno', 'dt_bldno'])
        #m.save()
        Machine.objects.filter(pk=m.id).update(
          last_status_update_time=m.last_status_update_time,
          status=m.status,
          pre_status=m.pre_status,
          os_bldno=m.os_bldno,
          dt_bldno=m.dt_bldno,
          bldno=m.bldno)

    if push_inst_log_flag:
        logid = log.get_logid(m.hostname)
        try:
            broadcast_channel(
              [{'channel': logid,
                'status': m.get_status_display(),
                'log': log.read_inst_log(m.hostname)},],
              channel=logid)
        except NoSocket, e:
            pass

    stat_infos = []        
    for k in push_status.keys():
        info = {'channel':  log.get_logid(m.hostname, k),
                'natrid': log.get_natrid(m.hostname, push_status[k].testrun_id),
                'status': push_status[k].get_status_display(),
                'log': log.read_test_log(m.hostname, k)}
        if push_status[k].log_path:
            info['rlogdir'] = push_status[k].log_path
        if push_status[k].report_id:
            info['trunid'] = push_status[k].report_id
        stat_infos.append(info)

        try:
            # push to channel(i.e. logid)
            broadcast_channel([info,], channel=info['channel'])
        except NoSocket, e:
            pass
        # sometimes the socket session id is lost due to reloading
        # ignore this exception to make sure client get response 
        except KeyError, e:
            print "WARNING: %s" % e

    if stat_infos:
        try:
            # push to channel group(i.e. test group)
            broadcast_channel(stat_infos, channel=log.get_testgid(m.hostname, m.bldno))
        except NoSocket, e:
            pass
        # sometimes the socket session id is lost due to reloading
        # ignore this exception to make sure client get response 
        except KeyError, e:
            print "WARNING: %s" % e

    return HttpResponse('success', content_type="text/plain")

